/*  ----------------------------------------------------------------------------
 *  Arello Mobile
 *  Fast 3d Engine
 *  ----------------------------------------------------------------------------
 * Except where otherwise noted, this work is licensed under
 * http://creativecommons.org/licenses/by/3.0
 */

#include "Application.h"
#include <lib3d/selector/Simple.h>
#include <lib3d/material/TextureSimple.h>
#include <lib3d/material/TextureAtlas.h>
#include <lib3d/material/TextureReflection.h>
#include <lib3d/material/TextureLoaderNotifier.h>
#include <lib3d/model/Model.h>
#include <lib3d/node/ModelFixed.h>
#include <lib3d/node/CameraPerspective.h>
#include <lib3d/model/ModelInHardware.h>

#ifdef MAC_VERSION
#include <input/hard/macx/Creator.h>
#endif

#ifdef WIN_VERSION
#include <input/hard/windows/creator.h>
#include <xml/ObjectSmart.h>
#endif

#if defined(ANDROID_NDK) || defined(SAMSUNG_VERSION)
#define MAC_VERSION
#define IPHONE_VERSION
#include <input/hard/macx/Creator.h>
#endif

namespace liba
{
namespace lib3d
{

#ifndef MAC_VERSION
static lib3d::hard::Hardware * sta_ha = 0;
static const int wnd_count = 6;
static HWND sh_wnd = 0;
static HWND ch_wnd = 0;

static HWND s_wnd[10] = {0};
static HWND c_wnd[10] = {0};
std::vector<std::string> c_names;

class StarterXML : public xml::ObjectSmart
{
public:
	class Dic : public xml::ObjectANSI
	{
	public:
		std::map<std::string, std::string> data;
		virtual bool on_attribute( xml::Provider * prov, const std::string & name, const std::string & value )
		{
			data[name] = value;
			return true;
		}
		const std::string & lookup(const std::string & str)const
		{
			std::map<std::string, std::string>::const_iterator it = data.find(str);
			if( it != data.end() )
				return it->second;
			return str;
		}
	};
	xml::Node<Dic> dictionary;
	xml::Node<Dic> filter;
	xml::Node<Dic> hide;

	StarterXML():
		dictionary( this, "dictionary" ),
		filter( this, "filter" ),
		hide( this, "hide" )
	{}
};

static StarterXML starter_xml;

static void CreatePropertiesBox(const hard::Strings & names)
{
	c_names.clear();
	SetWindowText(sh_wnd, starter_xml.dictionary.lookup("Adapter").c_str());
	for( int i = 0; i != names.size(); ++i )
	{
		if( starter_xml.hide.lookup(names[i]) != names[i] ) // default value
			continue;
		ShowWindow(s_wnd[c_names.size()], SW_SHOW);
		ShowWindow(c_wnd[c_names.size()], SW_SHOW);
		SetWindowText(s_wnd[c_names.size()], starter_xml.dictionary.lookup(names[i]).c_str());
		c_names.push_back( names[i] );
	}
	for( int c = c_names.size(); c < wnd_count; ++c )
	{
		ShowWindow(s_wnd[c], SW_HIDE);
		ShowWindow(c_wnd[c], SW_HIDE);
	}
}

static void FillCombo(HWND wnd, const hard::Strings & values)
{
	SendMessage(wnd, CB_RESETCONTENT, 0, 0);
	for(int j = 0; j != values.size(); ++j)
	{
		SendMessage( wnd, CB_ADDSTRING, 0, LPARAM(values[j].c_str()) );
	}
	EnableWindow(wnd, values.size() > 1);
}

void SelectComboValue(HWND wnd, const std::string & value);

static std::vector<std::string> split(std::string src, std::string separator)
{
	std::vector<std::string> result;
	if( separator.empty() )
		return result;

	int pos = 0;
	int prev_pos = 0;
	while( (pos = src.find( separator, prev_pos )) != std::string::npos )
	{
		result.push_back( src.substr( prev_pos, pos - prev_pos ) );
		prev_pos = pos + separator.size();
	}
	result.push_back( src.substr( prev_pos, src.size() - prev_pos ) );

	return result;
}

static void process_selchange(HWND wnd)
{
	if( wnd == ch_wnd )
	{
		int sel = SendMessage(ch_wnd, CB_GETCURSEL, 0, 0);
		if( sel != -1 )
		{
			int pos = 0;
			hard::Manager::HardwareList::const_iterator it = hard::Manager::get_hardwares().begin();
			for( ; it != hard::Manager::get_hardwares().end(); ++it, ++pos )
				if( pos == sel )
				{
					sta_ha = (*it);
					break;
				}
			CreatePropertiesBox(sta_ha->GetAttribute_names());
			for( int i = 0; i != c_names.size(); ++i )
			{
				FillCombo(c_wnd[i], sta_ha->GetAttribute_choices(c_names[i]));
			}
			SelectComboValue(c_wnd[0], sta_ha->GetAttribute(c_names[0]));
		}
	}
	else
	{
		int index = 0;
		for( ; index != c_names.size(); ++index )
			if( wnd == c_wnd[index] )
				break;
		if( index == c_names.size() )
			return;
		hard::Strings choices = sta_ha->GetAttribute_choices(c_names[index]);
		int sel = SendMessage(c_wnd[index], CB_GETCURSEL, 0, 0);
		if( sel != -1 )
		{
			sta_ha->set_attribute(c_names[index], choices[sel]);
			if( index != c_names.size() - 1 )
			{
				++index;
				choices = sta_ha->GetAttribute_choices(c_names[index]);
				FillCombo(c_wnd[index], choices);
				SelectComboValue(c_wnd[index], sta_ha->GetAttribute(c_names[index]));
			}
		}
	}
}

static void SelectComboValue(HWND wnd, const std::string & value)
{
	int id = SendMessage(wnd, CB_FINDSTRINGEXACT, -1, LPARAM(value.c_str()));
	SendMessage(wnd, CB_SETCURSEL, id, 0);
	process_selchange(wnd);
}

static INT_PTR CALLBACK AboutProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message)
	{
	case WM_INITDIALOG:
		{
			SetWindowText(hDlg, starter_xml.dictionary.lookup("DialogCaption").c_str());
			SetWindowText(GetDlgItem(hDlg, IDOK), starter_xml.dictionary.lookup("btnOK").c_str());
			SetWindowText(GetDlgItem(hDlg, IDCANCEL), starter_xml.dictionary.lookup("btnCancel").c_str());

			sh_wnd = GetDlgItem(hDlg, 1101);
			ch_wnd = GetDlgItem(hDlg, 1001);
			for(int i = 0; i != wnd_count; ++i)
			{
				s_wnd[i] = GetDlgItem(hDlg, 1102 + i);
				c_wnd[i] = GetDlgItem(hDlg, 1002 + i);
			}
			hard::Strings hards;
			hard::Manager::HardwareList::const_iterator it = hard::Manager::get_hardwares().begin();
			for( ; it != hard::Manager::get_hardwares().end(); ++it )
				hards.push_back( (*it)->get_name() );
			FillCombo(ch_wnd, hards);
			SendMessage(ch_wnd, CB_SETCURSEL, 0, 0);
			process_selchange(ch_wnd);
			return TRUE;
		}
	case WM_COMMAND:
		{
			int id = LOWORD(wParam);
			int notification = HIWORD(wParam);
			HWND not_wnd = HWND(lParam);
			if (id == IDOK || id == IDCANCEL)
			{
				EndDialog(hDlg, LOWORD(wParam));
				return TRUE;
			}
			if( notification == CBN_SELCHANGE )
				process_selchange(not_wnd);
			break;
		}
	}
	return FALSE;
}
#endif

Application::Application(const std::string & caption, const std::string & cmd_line)
:
#ifdef IPHONE_VERSION
WindowCreator(0),ApplicationWindowed( caption, cmd_line, 0 ),
#else
	ApplicationWindowed( caption, cmd_line ),
#endif
	cursor_camera( 0 ),
	cursor_node( 0 ),
	cursor_root( new node::Root(Atom("lib3d::Application::cursor_root")) ),
	previous_total_reps_to_load( 0 ),
	loading_screen_delay(1000),
	hardware( 0 )
{
	if(cmd_line.find("-nofullscreen") != std::string::npos)
	{
		hard::Manager::HardwareList::const_iterator it = hard::Manager::get_hardwares().begin();
		for( ; it != hard::Manager::get_hardwares().end(); ++it )
		{
			hard::Hardware * hardware = (*it);
			hardware->set_attribute( "Fullscreen", "No (Window mode)" );
		}
	}

	filesystem::String settings_filename = filesystem::String("%local_appdata%/") + caption.c_str() + "/videosettings.ini";
	selector::Simple sel;
#ifdef MAC_VERSION
	hardware = sel.load(settings_filename);
#else
	sta_ha = hardware = sel.load(settings_filename);

	if( cmd_line == "settings" )
	{
		boost::scoped_ptr<resource::Resource> res(resource::Manager::create_resource( "starter.ini" ));
		if( !xml::StreamResource(res.get()).load( &xml::Root( &starter_xml, "Starter" ) ) )
			logs::log() << "Starter.ini not found\n";

		hard::Manager::HardwareList::const_iterator it = hard::Manager::get_hardwares().begin();
		for( ; it != hard::Manager::get_hardwares().end(); ++it )
		{
			std::map<std::string, std::string>::const_iterator cit = starter_xml.filter.data.begin();
			for( ; cit != starter_xml.filter.data.end(); ++cit )
				(*it)->set_attribute_filter( cit->first, split(cit->second, "|") );
			cit = starter_xml.hide.data.begin();
			for( ; cit != starter_xml.hide.data.end(); ++cit )
				(*it)->set_attribute_filter( cit->first, split(cit->second, "|") );
			break;
		}

		if( DialogBox(GetModuleHandle(NULL), MAKEINTRESOURCE(103), get_window()->get_wnd(), AboutProc) == IDOK )
		{
			hardware = sta_ha;
			sel.save(sta_ha, settings_filename);
		}
		else
		{
			hardware = sel.load(settings_filename);
		}
	}
#endif
	if( !hardware || !hardware->set_mode( get_window() ) )
	{
		sel.change_mode( get_window() );
		hardware = sel.set_mode( get_window() );
	}

	if( !hardware )
		THROW(Gluck(__FUNCTION__, "Could not create desired videomode"));

	show_load_animation = false;
}

Application::Application(const std::string & caption, int sx, int sy, int resizeable )
:
#ifdef IPHONE_VERSION
	WindowCreator(0), ApplicationWindowed( caption, 0 ), //TODO: make it uniform
#else
	ApplicationWindowed( caption, sx, sy, resizeable ),
#endif
	cursor_camera( 0 ),
	cursor_node( 0 ),
	cursor_root( new node::Root(Atom("lib3d::Application::cursor_root")) ),
	previous_total_reps_to_load( 0 ),
	hardware( 0 )
{
	hard::Manager::HardwareList::const_iterator it = hard::Manager::get_hardwares().begin();
	for( ; it != hard::Manager::get_hardwares().end(); ++it )
	{
		hard::Hardware * hardware = (*it);
		hardware->set_attribute( "Fullscreen", "No (Window mode)" );
	}

	filesystem::String settings_filename = filesystem::String("%local_appdata%/") + caption.c_str() + "/videosettings.ini";

	selector::Simple sel;
	hardware = sel.load(settings_filename);

	if( !hardware || !hardware->set_mode( get_window() ) )
	{
		sel.change_mode( get_window() );
		hardware = sel.set_mode( get_window() );
	}

	if( !hardware )
		THROW(Gluck(__FUNCTION__, "Could not create desired videomode"));

	show_load_animation = false;
}

Application::~Application()
{
	delete cursor_root; cursor_root = 0;
	
	if( hardware )
		hardware->stop_mode();
	hardware = 0;
}

bool Application::LifeCycle( time_float delta_time )
{	
	cursor_animation.add_time_cycle( delta_time );
	if( cursor_node && cursor_camera )
	{
#ifdef MAC_VERSION
#ifdef IPHONE_VERSION
		double rel_x = 0;
		double rel_y = 0;
#else
		Point mouse_pt;
		GetGlobalMouse( &mouse_pt );
		GrafPtr saveport;
		GetPort( &saveport );
		SetPort( GetWindowPort( get_window()->get_wnd() ) );
		GlobalToLocal( &mouse_pt );
		SetPort( saveport );
		
		Rect bounds;
		GetWindowBounds( get_window()->get_wnd(), kWindowContentRgn, &bounds );
		input::hard::macx::Creator::resize( bounds.bottom - bounds.top, bounds.right - bounds.left );

		double rel_x = 1.0*mouse_pt.h/(bounds.right - bounds.left);
		double rel_y = 1.0*mouse_pt.v/(bounds.bottom - bounds.top);
#endif
#else
		POINT point;
		GetCursorPos(&point);
		ScreenToClient( get_window()->get_wnd(), &point );
		RECT rect;		
		GetClientRect( get_window()->get_wnd(), &rect );
		input::hard::windows::Creator::resize( rect.bottom - rect.top, rect.right - rect.left );
		double rel_x = 1.0*point.x/(rect.right - rect.left);
		double rel_y = 1.0*point.y/(rect.bottom - rect.top);
#endif
		Vector pos = cursor_node->GetPosition();

		double aspect( 1.0 );
		double fov( 1.0 );
		bool result( true );
		result = result && cursor_camera->FindProperty<double>( node::CameraPerspective::aspect_name );
		if ( result )
			aspect = cursor_camera->FindProperty<double>( node::CameraPerspective::aspect_name )->Get();
		result = result && cursor_camera->FindProperty<double>( node::CameraPerspective::fov_name );
		if ( result )
			fov = cursor_camera->FindProperty<double>( node::CameraPerspective::fov_name )->Get();
		if( result )
		{
			double h = ( cos( fov/2 ) / sin( fov/2 ) );
			double w = aspect * h;
			pos[0] = pos.z * (rel_x - 0.5) * 2 / w;
			pos[1] = pos.z * (rel_y - 0.5) * 2 / h;
		}
		else
		{
			double width( 0.0 );
			double height( 0.0 );
			bool result( true );
			result = result && cursor_camera->FindProperty<double>( node::CameraParallel::width_name );
			if ( result )
				width = cursor_camera->FindProperty<double>( node::CameraParallel::width_name )->Get();
			result = result && cursor_camera->FindProperty<double>( node::CameraParallel::height_name );
			if ( result )
				height = cursor_camera->FindProperty<double>( node::CameraParallel::height_name )->Get();

			if ( !result )
			{
				cursor_camera->FindProperty<double>( node::CameraParallel::width_name )->Set( width );
				cursor_camera->FindProperty<double>( node::CameraParallel::height_name )->Set( height );
			}

			pos[0] = (rel_x  - 0.5)* width;
			pos[1] = (0.5 - rel_y) * height;
		}
		cursor_node->ChangePosition( pos );
	}

	material::TextureLoadingManager::GetInstance()->LifeCycle();

	int total_reps_to_load = 0;

	__int64 start_load_time = threads::get_precise_tick_count();
	float time_to_load = show_load_animation ? 0.1 : 1;

	total_reps_to_load += material::TextureSimple::load_reps(start_load_time, time_to_load);
	total_reps_to_load += material::TextureAtlas::load_reps(start_load_time, time_to_load);
	total_reps_to_load += material::TextureReflection::load_reps(start_load_time, time_to_load);
	total_reps_to_load += SharedPrecached<filesystem::String, model::ModelInHardware>::load_reps(start_load_time, time_to_load);
	total_reps_to_load += model::ModelInHardware::load_reps(start_load_time, time_to_load);

	total_reps_to_load += material::TextureSequence::load_reps(start_load_time, time_to_load);
	total_reps_to_load += material::TextureSequenceFrame::load_reps(start_load_time, time_to_load);

	static __int64 time_load_start = 0;
	if( !show_load_animation && total_reps_to_load != 0 )
	{
		if(!time_load_start)
			time_load_start = threads::get_precise_tick_count();
		else
		{
			int delta = (threads::get_precise_tick_count() - time_load_start);
			show_load_animation = delta > loading_screen_delay;
		}

		if(show_load_animation)
			on_loading_start();
	}
	previous_total_reps_to_load = std::max( previous_total_reps_to_load, total_reps_to_load );
	if( show_load_animation && previous_total_reps_to_load != 0 )
	{
		on_loading_progress( double(previous_total_reps_to_load - total_reps_to_load)/previous_total_reps_to_load );
#ifndef IPHONE_VERSION
		threads::sleep( 20 ); // Allow hard drive to load models
#endif
	}
	if( previous_total_reps_to_load != 0 && total_reps_to_load == 0 )
	{
		time_load_start = 0;
		if(show_load_animation)
		{
			show_load_animation = false;
			on_loading_finish();
		}
		previous_total_reps_to_load = 0;
	}
	
	//node::NodeBase::report_usage();
	//hard::TextureBase::report_usage();
	//hard::VertexBuffer::report_usage();

	return ApplicationWindowed::LifeCycle( delta_time );
}

int Application::window_proc( unsigned msg, unsigned wParam, unsigned lParam )
{
#ifndef MAC_VERSION
	switch (msg) 
	{
	case WM_KEYDOWN:
		{
/*			if ( wParam == VK_F1 && false ) // Evil GAG
			{
				Lock lock( render_lock );
				suspend();
				ShowWindow( get_main_wnd()->get_wnd(), SW_HIDE );
				try
				{
					if( hardware )
						hardware->stop_mode();
					hardware = 0;

					selector->change_mode( get_main_wnd() );
					hardware = selector->set_mode( get_main_wnd() );
					if( !hardware )
						quit("hardware=0 after changing mode", -1);
				}
				catch(...)
				{
					MessageBox( get_main_wnd()->get_wnd(), "Error Switching To New Mode", "Error", MB_OK | MB_ICONSTOP );
					quit("an exception cathed while setting the new mode", -1);
				}
				ShowWindow( get_main_wnd()->get_wnd(), SW_SHOW );
				resume();
			}*/
		}
		break;
	}

	return ApplicationWindowed::window_proc( msg, wParam, lParam );
#endif
	return 0;
}

void Application::change_cursor(const filesystem::String & scene)
{
	if( cursor_scene == scene )return;
	cursor_scene = scene;
	cursor_camera = 0;
	cursor_node = 0;
	cursor_root->Clear();
	cursor_animation = animate::Animation();
	if( scene != filesystem::String() )
	{
		cursor_camera = load_root( cursor_root, &cursor_animation, scene, Atom("Camera") );
		cursor_node = cursor_root->FindNode( Atom("Cursor"), true );
		if( !cursor_node )
			THROW(GluckBadResource(__FUNCTION__, "Cursor node not found", scene + ".scene" ));
	}
}

const filesystem::String Application::get_cursor()const
{
	return cursor_scene;
}

void Application::render_cursor( hard::RenderTarget * rt )
{
	if( cursor_camera )
	{
		lib3d::hard::RenderList render_list;
		cursor_camera->start_render_to_list( hardware, &render_list );
		render_list.clear_render_target = false;
		hardware->render( &render_list, rt );
	}
}

node::Camera * Application::load_root(node::Root * root, animate::Animation * animation, const filesystem::String & base_name, const Atom & camera_name )
{
	if( !root )
		return 0;
	boost::scoped_ptr<resource::Resource> scene_res(resource::Manager::create_resource( base_name + ".scene" ));
	if( !xml::StreamResource( scene_res.get() ).load( &xml::Root(root,"Scene") ) )
		THROW(GluckBadResource(__FUNCTION__, "load() failed", base_name + ".scene"));

	node::Camera * camera = root->find_camera( camera_name );
	if( !camera )
		THROW(GluckBadResource(__FUNCTION__, "Camera not found", base_name + ".scene"));

	if( animation )
	{
		boost::scoped_ptr< resource::Resource > anim_res(resource::Manager::create_resource( base_name + ".anim" ));
		if( !xml::StreamResource( anim_res.get() ).load( &xml::Root(animation, "Animation") ) )
			THROW(GluckBadResource(__FUNCTION__, "load() failed", base_name + ".anim"));
		if( !animation->bind( root, false ) )
		{}//	throw GluckBadResource(__FUNCTION__, "bind failed", base_name + ".anim");
	}
	return camera;
}

#undef MAC_VERSION
#undef IPHONE_VERSION


} // namespace lib3d
} // namespace liba

